<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第七章：Indexes模块 (数据增强生成 RAG)</title>
    <link rel="stylesheet" href="../shared_styles.css">
</head>
<body>
    <div class="container">
        <h1> 第七章：Indexes模块 (数据增强生成 RAG)</h1>

        <section id="s1">
            <h2>1. Indexes模块与RAG概述</h2>
            <p>大型语言模型（LLMs）虽然知识渊博，但它们的知识通常截止于其训练数据的最后日期，并且不包含私有或特定领域的信息。为了让LLM能够回答关于特定文档或最新信息的问题，我们需要一种方法将这些外部数据“喂”给LLM。这就是<strong>检索增强生成（Retrieval Augmented Generation, RAG）</strong>的核心思想。</p>
            <p>Langchain的<strong>Indexes（索引）</strong>模块提供了一整套工具来构建RAG流程。其主要目标是：</p>
            <ul>
                <li><strong>加载数据（Loading）：</strong> 从各种来源（如文本文件、PDF、网页、数据库等）加载文档。</li>
                <li><strong>分割数据（Splitting）：</strong> 将加载的文档分割成更小的、语义相关的块（Chunks），以便于嵌入和检索。</li>
                <li><strong>存储数据（Storing）：</strong> 为这些文本块创建向量嵌入（Embeddings），并将文本块及其嵌入存储到向量数据库（Vector Store）中。</li>
                <li><strong>检索数据（Retrieving）：</strong> 当用户提出问题时，将问题也转换为向量嵌入，然后在向量数据库中搜索与问题嵌入最相似的文本块。</li>
                <li><strong>生成答案（Generating）：</strong> 将检索到的相关文本块与原始问题一起作为上下文信息，构建一个新的提示（Prompt），并将其发送给LLM以生成最终答案。</li>
            </ul>
            <p>通过这种方式，LLM的回答就不仅仅依赖于其内部知识，而是基于我们提供的最新或特定领域的数据。</p>
        </section>

        <section id="s2">
            <h2>2. RAG的核心组件</h2>
            <p>一个典型的RAG流程涉及以下Langchain组件：</p>
            <ul>
                <li><strong>Document Loaders (文档加载器):</strong> 用于从不同来源加载文档数据。</li>
                <li><strong>Text Splitters (文本分割器):</strong> 用于将长文档分割成小块。</li>
                <li><strong>Text Embedding Models (文本嵌入模型):</strong> 用于将文本块转换为向量嵌入。</li>
                <li><strong>Vector Stores (向量存储):</strong> 用于存储文本块的嵌入并支持高效的相似性搜索。</li>
                <li><strong>Retrievers (检索器):</strong> 负责根据用户查询从向量存储中检索相关文档块。</li>
            </ul>
            <p>我们将逐一介绍这些组件，并使用Qwen模型进行演示。</p>
        </section>

        <section id="s3">
            <h2>3. Document Loaders (文档加载器)</h2>
            <p>文档加载器负责从各种来源读取数据并将其转换为Langchain可以处理的<code>Document</code>对象。一个<code>Document</code>对象通常包含<code>page_content</code>（文本内容）和<code>metadata</code>（元数据，如来源、页码等）。</p>
            <p>Langchain提供了大量的内置文档加载器，例如：</p>
            <ul>
                <li><code>TextLoader</code>: 加载纯文本文件。</li>
                <li><code>PyPDFLoader</code>: 加载PDF文件。</li>
                <li><code>WebBaseLoader</code>: 加载网页内容。</li>
                <li><code>CSVLoader</code>: 加载CSV文件。</li>
                <li>还有用于Notion, YouTube, Wikipedia, ArXiv等的加载器。</li>
            </ul>
            <p><strong>示例：使用TextLoader加载本地文本文件</strong></p>
            <pre><code class="language-python">
from langchain_community.document_loaders import TextLoader
import os

# 假设我们有一个名为 "my_document.txt" 的文件
# 先创建一个示例文件
sample_text_content = """Langchain是一个强大的框架。
它帮助开发者构建基于LLM的应用。
RAG是Langchain的一个重要应用场景。
"""
with open("my_document.txt", "w", encoding="utf-8") as f:
    f.write(sample_text_content)

# 1. 初始化TextLoader
loader = TextLoader("./my_document.txt", encoding="utf-8")

# 2. 加载文档
documents = loader.load()

print("--- TextLoader 示例 ---")
print(f"加载的文档数量: {len(documents)}")
for doc in documents:
    print(f"内容:\n{doc.page_content}")
    print(f"元数据: {doc.metadata}")

# 清理示例文件
# os.remove("my_document.txt")
            </code></pre>
            <p><strong>示例：使用PyPDFLoader加载PDF文件</strong> (需要安装 <code>pypdf</code>: <code>pip install pypdf</code>)</p>
            <pre><code class="language-python">
# from langchain_community.document_loaders import PyPDFLoader

# 假设你有一个名为 "sample.pdf" 的PDF文件
# loader_pdf = PyPDFLoader("./sample.pdf") # 你需要提供一个实际的PDF文件路径
# pages = loader_pdf.load_and_split() # 加载并按页分割

# print("\n--- PyPDFLoader 示例 ---")
# if pages:
#     print(f"PDF总页数 (作为独立文档加载): {len(pages)}")
#     print(f"第一页内容片段: {pages[0].page_content[:200]}...") # 打印第一页前200字符
#     print(f"第一页元数据: {pages[0].metadata}")
# else:
#     print("未能加载PDF文件，请确保文件路径正确且内容可读。")
            </code></pre>
            <p class="note">请确保你有一个实际的PDF文件用于测试<code>PyPDFLoader</code>。</p>
        </section>

        <section id="s4">
            <h2>4. Text Splitters (文本分割器)</h2>
            <p>LLM通常有上下文窗口限制（即一次能处理的Token数量有限）。因此，在将文档内容传递给嵌入模型或LLM之前，通常需要将长文档分割成更小的、语义上连贯的块（Chunks）。</p>
            <p>文本分割器帮助我们实现这一点。Langchain提供了多种分割策略：</p>
            <ul>
                <li><strong><code>CharacterTextSplitter</code>:</strong> 按固定数量的字符进行分割，可以指定字符重叠（overlap）以保持块之间的连贯性。</li>
                <li><strong><code>RecursiveCharacterTextSplitter</code>:</strong> 推荐的通用分割器。它尝试按一系列分隔符（如<code>\n\n</code>, <code>\n</code>, <code> </code>, <code>""</code>）递归地分割文本，以尽可能保持段落、句子等语义单元的完整性。</li>
                <li><strong><code>TokenTextSplitter</code>:</strong> 根据Token数量进行分割（需要Token计算器，如<code>tiktoken</code>）。</li>
                <li>针对特定格式（如Markdown, Python代码）的分割器。</li>
            </ul>
            <p><strong>示例：使用RecursiveCharacterTextSplitter</strong></p>
            <pre><code class="language-python">
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 假设我们有之前加载的文档 (documents[0] from TextLoader)
# 或者直接使用一个长字符串
long_text = """Langchain是一个用于开发由语言模型驱动的应用程序的框架。
它使得开发者能够轻松地将大型语言模型（LLM）与其他计算或知识来源集成。
核心组件包括模型I/O、检索、链、代理、内存和回调。
这些组件是模块化的，可以组合使用来创建复杂的应用程序。
例如，可以创建一个链，首先从用户输入生成一个提示，然后将该提示发送给LLM，最后处理LLM的输出。
RAG（检索增强生成）是Langchain的一个关键应用，它允许LLM访问外部数据以提供更准确和最新的答案。
"""
# 重新创建示例文件内容给documents变量
doc_for_splitting = [type('Document', (object,), {'page_content': long_text, 'metadata': {'source': 'example_text'}})]


# 1. 初始化RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,  # 每个块的目标大小（字符数）
    chunk_overlap=20, # 块之间的重叠字符数
    length_function=len, # 用于计算长度的函数
    is_separator_regex=False, # 分隔符是否为正则表达式
)

# 2. 分割文档
# 如果是单个Document对象
chunks = text_splitter.split_documents(doc_for_splitting) 
# 如果是纯文本字符串
# text_chunks = text_splitter.split_text(long_text)

print("\n--- RecursiveCharacterTextSplitter 示例 ---")
print(f"原始文档数量: {len(doc_for_splitting)}")
print(f"分割后的块数量: {len(chunks)}")
for i, chunk in enumerate(chunks):
    print(f"--- 块 {i+1} ---")
    print(f"内容:\n{chunk.page_content}")
    print(f"长度: {len(chunk.page_content)}")
    print(f"元数据: {chunk.metadata}") # 元数据会被继承
            </code></pre>
            <p>选择合适的<code>chunk_size</code>和<code>chunk_overlap</code>对于RAG的性能至关重要。块太小可能丢失上下文，块太大可能超出模型限制或包含过多无关信息。</p>
        </section>

        <section id="s5">
            <h2>5. Text Embedding Models (文本嵌入模型)</h2>
            <p>在第二章中我们已经介绍过文本嵌入模型。它们将文本块转换为数值向量，捕捉文本的语义含义。这些嵌入是向量存储和相似性搜索的基础。</p>
            <p>我们将使用之前提到的阿里云DashScope的嵌入模型（通过<code>DashScopeEmbeddings</code>）。</p>
            <pre><code class="language-python">
from langchain_community.embeddings import DashScopeEmbeddings
from dotenv import load_dotenv
import os

load_dotenv()
api_key = os.environ.get("DASHSCOPE_API_KEY")
if not api_key:
    raise ValueError("DASHSCOPE_API_KEY not found in environment variables.")

# 初始化DashScopeEmbeddings
qwen_embeddings = DashScopeEmbeddings(
    dashscope_api_key=api_key,
    model="text-embedding-v1" # 例如 text-embedding-v1
)

# 假设我们有之前分割好的文本块 (chunks)
# 这里我们只嵌入前两个块作为示例
if 'chunks' in globals() and len(chunks) >= 2:
    chunk_texts_to_embed = [chunks[0].page_content, chunks[1].page_content]
    
    print("\n--- Text Embedding 示例 ---")
    # 嵌入单个查询
    query_example = "什么是Langchain的核心组件？"
    query_embedding = qwen_embeddings.embed_query(query_example)
    print(f"查询 '{query_example}' 的嵌入向量 (前5维): {query_embedding[:5]}")
    print(f"嵌入向量维度: {len(query_embedding)}")

    # 嵌入多个文档块
    document_embeddings = qwen_embeddings.embed_documents(chunk_texts_to_embed)
    print(f"\n嵌入了 {len(document_embeddings)} 个文档块。")
    print(f"第一个文档块的嵌入 (前5维): {document_embeddings[0][:5]}")
else:
    print("\n--- Text Embedding 示例 ---")
    print("跳过嵌入文档块的示例，因为 'chunks' 未定义或数量不足。")
            </code></pre>
        </section>

        <section id="s6">
            <h2>6. Vector Stores (向量存储)</h2>
            <p>向量存储（或向量数据库）用于存储文本块的嵌入向量，并提供高效的相似性搜索功能。当用户提问时，问题被嵌入，然后在向量存储中查找与问题嵌入最相似的文档块嵌入。</p>
            <p>Langchain集成了多种向量存储，包括：</p>
            <ul>
                <li><strong>FAISS:</strong> 一个用于高效相似性搜索和密集向量聚类的库（Facebook AI Research）。适合本地或内存中运行。 (<code>pip install faiss-cpu</code> 或 <code>faiss-gpu</code>)</li>
                <li><strong>Chroma:</strong> 一个开源的嵌入数据库，易于使用。 (<code>pip install chromadb</code>)</li>
                <li><strong>Pinecone, Weaviate, Milvus:</strong> 托管的或可自部署的生产级向量数据库。</li>
                <li>还有很多其他选项...</li>
            </ul>
            <p><strong>示例：使用FAISS作为向量存储</strong></p>
            <pre><code class="language-python">
from langchain_community.vectorstores import FAISS
# (qwen_embeddings 和 chunks 变量应已定义并包含数据)

print("\n--- FAISS Vector Store 示例 ---")
if 'chunks' in globals() and chunks and 'qwen_embeddings' in globals():
    # 1. 从文档块和嵌入模型创建FAISS索引
    # FAISS.from_documents() 会自动为文档块生成嵌入并构建索引
    print("正在创建FAISS索引...")
    try:
        vector_store_faiss = FAISS.from_documents(documents=chunks, embedding=qwen_embeddings)
        print("FAISS索引创建成功！")

        # 2. 进行相似性搜索
        query_for_search = "Langchain的核心组件有哪些？"
        print(f"\n使用查询进行相似性搜索: '{query_for_search}'")
        # k=2 表示返回最相似的2个文档块
        retrieved_docs_faiss = vector_store_faiss.similarity_search(query=query_for_search, k=2)
        
        print(f"\n检索到的文档数量: {len(retrieved_docs_faiss)}")
        for i, doc in enumerate(retrieved_docs_faiss):
            print(f"--- 检索到的文档 {i+1} ---")
            print(f"内容片段: {doc.page_content[:150]}...")
            print(f"元数据: {doc.metadata}")

        # 3. 也可以进行带分数的相似性搜索
        retrieved_docs_with_scores = vector_store_faiss.similarity_search_with_score(query=query_for_search, k=2)
        print("\n检索到的文档 (带分数，越低越相似 for L2, 越高越相似 for cosine):")
        for doc, score in retrieved_docs_with_scores:
            print(f"分数: {score:.4f} - 内容片段: {doc.page_content[:100]}...")
            
        # 4. 将索引保存到本地 (可选)
        # vector_store_faiss.save_local("my_faiss_index")
        # 加载本地索引:
        # loaded_vector_store = FAISS.load_local("my_faiss_index", embeddings=qwen_embeddings, allow_dangerous_deserialization=True)

    except Exception as e:
        print(f"创建或使用FAISS索引时出错: {e}")
        print("请确保已安装 faiss-cpu 或 faiss-gpu，并且 'chunks' 和 'qwen_embeddings' 已正确初始化。")

else:
    print("跳过FAISS示例，因为 'chunks' 或 'qwen_embeddings' 未定义或为空。")
            </code></pre>
            <p class="note">FAISS的<code>similarity_search_with_score</code>返回的score，对于使用余弦相似度的嵌入（如很多Transformer嵌入），分数越高表示越相似。对于L2距离（欧氏距离），分数越低表示越相似。DashScope的嵌入模型通常使用余弦相似度。</p>
        </section>

        <section id="s7">
            <h2>7. Retrievers (检索器)</h2>
            <p>检索器是一个更通用的接口，它不一定需要存储文档，其核心功能是根据查询返回相关的<code>Document</code>对象。向量存储通常可以很容易地转换为检索器（使用<code>.as_retriever()</code>方法）。</p>
            <p>检索器可以配置搜索参数，如搜索类型（相似性、MMR等）和返回的文档数量（<code>k</code>）。</p>
            <p><strong>示例：从FAISS向量存储创建检索器</strong></p>
            <pre><code class="language-python">
# (vector_store_faiss 应已创建)
print("\n--- Retriever 示例 ---")
if 'vector_store_faiss' in globals() and vector_store_faiss:
    # 1. 从向量存储创建检索器
    retriever_faiss = vector_store_faiss.as_retriever(
        search_type="similarity", # "similarity", "mmr", "similarity_score_threshold"
        search_kwargs={"k": 3}    # 返回最相似的3个文档块
    )

    # 2. 使用检索器获取相关文档
    query_for_retriever = "RAG在Langchain中如何工作？"
    retrieved_by_retriever = retriever_faiss.invoke(query_for_retriever) # invoke是LCEL标准方法
    # 或者 retriever_faiss.get_relevant_documents(query_for_retriever) # 旧版方法

    print(f"\n使用检索器查询: '{query_for_retriever}'")
    print(f"检索到的文档数量: {len(retrieved_by_retriever)}")
    for i, doc in enumerate(retrieved_by_retriever):
        print(f"--- 检索到的文档 {i+1} (来自Retriever) ---")
        print(f"内容片段: {doc.page_content[:150]}...")
else:
    print("跳过Retriever示例，因为 'vector_store_faiss' 未定义。")
    retriever_faiss = None #确保 retriever_faiss 在此路径中也被定义
            </code></pre>
            <p>除了基本的相似性搜索，检索器还可以支持更高级的检索策略，如最大边际相关性（Maximal Marginal Relevance, MMR），它试图在保持相关性的同时增加结果的多样性。</p>
        </section>

        <section id="s8">
            <h2>8. 构建一个简单的RAG应用 (Chain)</h2>
            <p>现在我们将所有组件组合起来，使用Langchain表达式语言（LCEL）构建一个简单的RAG应用链。这个链将：</p>
            <ol>
                <li>接收用户问题。</li>
                <li>使用检索器获取相关文档。</li>
                <li>将问题和检索到的文档格式化为一个新的提示。</li>
                <li>将该提示发送给Qwen LLM以生成答案。</li>
            </ol>
            <pre><code class="language-python">
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate # Removed HumanMessagePromptTemplate, SystemMessagePromptTemplate as they are not directly used for from_template
from langchain_core.runnables import RunnablePassthrough # Removed RunnableLambda as it's not used
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
import os

# qwen_embeddings (from section 5) and retriever_faiss (from section 7) 
# should be defined if those previous sections executed successfully.
# qwen_llm will be defined in this section.
load_dotenv()
api_key = os.environ.get("DASHSCOPE_API_KEY")

qwen_llm = None # Initialize qwen_llm to None by default
if not api_key:
    print("错误: DASHSCOPE_API_KEY 未设置。RAG链的LLM部分无法初始化。")
    # qwen_llm remains None
    # retriever_faiss's status depends on the successful execution of section 7
else:
    try:
        qwen_llm = ChatOpenAI(
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            api_key=api_key,
        model="qwen-plus", 
        temperature=0.3 # RAG通常用较低温度以保证答案基于上下文
        )
        print("RAG Chain için Qwen LLM başarıyla başlatıldı.") # Info message for successful LLM init
    except Exception as e:
        print(f"RAG chain için Qwen LLM başlatılırken hata oluştu: {e}")
        qwen_llm = None # Ensure qwen_llm is None if initialization fails

print("\n--- 构建RAG Chain 示例 ---")

# Check if retriever_faiss was defined and is not None from previous steps
retriever_is_available = 'retriever_faiss' in globals() and retriever_faiss is not None

if qwen_llm and retriever_is_available:
    # 1. 定义RAG提示模板
    rag_template_str = """
    你是一个问答助手。请根据以下提供的上下文信息来回答用户的问题。
    如果上下文中没有足够的信息来回答问题，请明确说明你无法从已知信息中找到答案，不要编造。
    保持答案简洁。

    上下文:
    {context}

    用户问题:
    {question}

    回答:
    """
    rag_prompt = ChatPromptTemplate.from_template(rag_template_str)

    # 2. 定义一个函数来格式化检索到的文档作为上下文
    def format_docs_for_context(docs: list) -> str:
        return "\n\n".join([d.page_content for d in docs])

    # 3. 构建RAG链 (使用LCEL)
    rag_chain = (
        # RunnablePassthrough.assign 会将新的键值对添加到传递的字典中
        # retriever_faiss.invoke(x["question"]) 会执行检索
        RunnablePassthrough.assign(context=(lambda x: format_docs_for_context(retriever_faiss.invoke(x["question"]))))
        | rag_prompt
        | qwen_llm
        | StrOutputParser() # 将LLM的AIMessage输出转换为字符串
    )

    # 4. 测试RAG链
    user_question_for_rag = "Langchain的核心组件有哪些？它们是如何协同工作的？"
    print(f"\n用户问题 (RAG): {user_question_for_rag}")
    
    # 调用RAG链
    # rag_chain期望一个包含 "question" 键的字典作为输入
    rag_answer = rag_chain.invoke({"question": user_question_for_rag})
    
    print(f"\nRAG Chain 回答:\n{rag_answer}")

    user_question_for_rag_2 = "FAISS是什么？它在Langchain中有什么用？"
    print(f"\n用户问题 (RAG): {user_question_for_rag_2}")
    rag_answer_2 = rag_chain.invoke({"question": user_question_for_rag_2})
    print(f"\nRAG Chain 回答:\n{rag_answer_2}")
    
    user_question_for_rag_3 = "恐龙是怎么灭绝的？" # 这个问题可能不在我们的示例文档中
    print(f"\n用户问题 (RAG): {user_question_for_rag_3}")
    rag_answer_3 = rag_chain.invoke({"question": user_question_for_rag_3})
    print(f"\nRAG Chain 回答:\n{rag_answer_3}")

else:
    print("跳过RAG Chain示例，因为其依赖的组件未能成功初始化。具体原因如下：")
    if not qwen_llm:
        print("- LLM (qwen_llm) 未能初始化。这通常是由于 DASHSCOPE_API_KEY 未设置或无效，或者网络连接问题。")
    if not retriever_is_available:
        print("- Retriever (retriever_faiss) 未能初始化。请检查第七节 (Retrievers) 的代码是否成功执行，以及其依赖的 vector_store_faiss (来自第六节) 是否已正确创建。")
            </code></pre>
            <p>在这个LCEL链中：</p>
            <ul>
                <li><code>RunnablePassthrough.assign(context=...)</code>: 接收包含<code>question</code>的输入字典，调用检索器获取相关文档，格式化这些文档，然后将结果以<code>context</code>键添加到字典中，并将此更新后的字典传递给下一步。</li>
                <li><code>rag_prompt</code>: 接收包含<code>question</code>和<code>context</code>的字典，并生成格式化的提示。</li>
                <li><code>qwen_llm</code>: 接收提示并生成AI消息。</li>
                <li><code>StrOutputParser()</code>: 将AI消息的内容提取为字符串。</li>
            </ul>
            <p>这就是一个基本的RAG流程。你可以通过优化提示、调整检索参数、使用更高级的检索策略（如ParentDocumentRetriever、SelfQueryRetriever等）来进一步改进它。</p>
        </section>

        <section id="s9">
            <h2>9. 总结与后续</h2>
            <p>Indexes模块和RAG是Langchain中非常强大的功能，它们使得LLM能够利用外部知识库来提供更准确、更相关、更及时的回答。通过组合文档加载器、文本分割器、嵌入模型、向量存储和检索器，你可以为你的LLM应用构建强大的数据基础。</p>
            <p>后续可以探索的主题包括：</p>
            <ul>
                <li>更高级的检索策略和检索器。</li>
                <li>评估RAG系统的性能。</li>
                <li>处理大规模数据集的索引和检索。</li>
                <li>将RAG与Agents结合，让Agent能够自主查询外部知识。</li>
            </ul>
        </section>

        <div class="navigation">
            <a href="../langchain_tutorial_outline.html">返回教程目录</a>
            <a href="../chapter6/chapter6_memory.html">上一章：Memory模块</a>
            <a href="../chapter8/chapter8_callbacks.html">下一章：Callbacks模块</a>
        </div>
    </div>
</body>
</html>
